在設計 RESTful API 時,提供一致且資訊豐富的回應結構對於提高 API 的可用性和可維護性至關重要
今天,我們將探討如何設計一個統一的 API 回應結構,並基於之前的 Todo List API 範例進行改寫
RFC 9457 定義了一種用於在 HTTP API 中傳遞問題詳情的標準格式,稱為「問題詳情(Problem Details)」
這個標準旨在提供一個一致的方法來描述 API 中發生的錯誤情況,使得客戶端能夠更好地理解錯誤的性質和處理方法
要注意一下,這主題有可能會查到 RFC 7807 的相關內容,這是因為 RFC 9457 (發佈於 2023 年) 是 RFC 7807 (發佈於 2016 年) 的一個更新版本
它保留了原始規範的核心概念,同時增加了新的功能和釐清了一些觀點
結構化格式
主要內容
擴展性
讓我們建立一個 ApiResponse 類別來封裝我們的 API 回應格式
public class ApiResponse<T> {
private boolean success;
private T data;
private ErrorDetails error;
// 建構子、getter 和 setter 省略
public static class ErrorDetails {
private String type;
private String title;
private int status;
private String detail;
private String instance;
// 建構子、getter 和 setter 省略
}
}
這個 ApiResponse 類別主要包含三個部分:
現在,讓我們修改 TodoController 以使用這個新的 API 回應格式
@RestController
@RequestMapping("/api/todos")
public class TodoController {
private static final List<Todo> todos = new ArrayList<>();
private static final AtomicLong idCounter = new AtomicLong();
@PostMapping
public ResponseEntity<ApiResponse<Todo>> createTodo(@RequestBody Todo todo) {
long id = idCounter.incrementAndGet();
todo.setId(id);
todos.add(todo);
return ResponseEntity.ok(new ApiResponse<>(true, todo, null));
}
@GetMapping
public ResponseEntity<ApiResponse<List<Todo>>> getAllTodos() {
return ResponseEntity.ok(new ApiResponse<>(true, todos, null));
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Todo>> getTodo(@PathVariable Long id) {
Optional<Todo> todo = todos.stream()
.filter(t -> t.getId().equals(id))
.findFirst();
if (todo.isPresent()) {
return ResponseEntity.ok(new ApiResponse<>(true, todo.get(), null));
} else {
ApiResponse.ErrorDetails error = new ApiResponse.ErrorDetails(
"https://example.com/errors/not-found",
"Todo not found",
HttpStatus.NOT_FOUND,
MessageFormat.format("Todo with id {0} does not exist", id),
MessageFormat.format("/api/todos/{0}", id)
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiResponse<>(false, null, error));
}
}
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<Object>> updateTodo(@PathVariable Long id, @RequestBody Todo updatedTodo) {
for (int i = 0; i < todos.size(); i++) {
if (todos.get(i).getId().equals(id)) {
updatedTodo.setId(id);
todos.set(i, updatedTodo);
return ResponseEntity.ok(new ApiResponse<>(true, updatedTodo, null));
}
}
ApiResponse.ErrorDetails error = new ApiResponse.ErrorDetails(
"https://example.com/errors/not-found",
"Todo not found",
HttpStatus.NOT_FOUND,
MessageFormat.format("Todo with id {0} does not exist", id),
MessageFormat.format("/api/todos/{0}", id)
);
return ResponseEntity.status(404).body(new ApiResponse<>(false, null, error));
}
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Object>> deleteTodo(@PathVariable Long id) {
boolean isSuccess = todos.removeIf(todo -> todo.getId().equals(id));
if (isSuccess) {
return ResponseEntity.ok(new ApiResponse<>(true, null, null));
}
ApiResponse.ErrorDetails error = new ApiResponse.ErrorDetails(
"https://example.com/errors/not-found",
"Todo not found",
HttpStatus.NOT_FOUND,
MessageFormat.format("Todo with id {0} does not exist", id),
MessageFormat.format("/api/todos/{0}", id)
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiResponse<>(false, null, error));
}
}
ResponseEntity<ApiResponse<T>>
,這允許我們設置 HTTP 狀態碼和回應的數據200 OK
狀態碼,並在 ApiResponse 中設置 success 為 true404 Not Found
狀態碼,並在 ApiResponse 中設置 success 為 false另外,可以看到 not found 的部分已經重複了,可以再把一樣的部分拉出變共用的 method
@RestController
@RequestMapping("/api/todos")
public class TodoController {
private static final List<Todo> todos = new ArrayList<>();
private static final AtomicLong idCounter = new AtomicLong();
@PostMapping
public ResponseEntity<ApiResponse<Todo>> createTodo(@RequestBody Todo todo) {
long id = idCounter.incrementAndGet();
todo.setId(id);
todos.add(todo);
return ResponseEntity.ok(new ApiResponse<>(true, todo, null));
}
@GetMapping
public ResponseEntity<ApiResponse<List<Todo>>> getAllTodos() {
return ResponseEntity.ok(new ApiResponse<>(true, todos, null));
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Todo>> getTodo(@PathVariable Long id) {
Optional<Todo> todo = todos.stream()
.filter(t -> t.getId().equals(id))
.findFirst();
if (todo.isPresent()) {
return ResponseEntity.ok(new ApiResponse<>(true, todo.get(), null));
} else {
return createNotFoundError(id);
}
}
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<Todo>> updateTodo(@PathVariable Long id, @RequestBody Todo updatedTodo) {
for (int i = 0; i < todos.size(); i++) {
if (todos.get(i).getId().equals(id)) {
updatedTodo.setId(id);
todos.set(i, updatedTodo);
return ResponseEntity.ok(new ApiResponse<>(true, updatedTodo, null));
}
}
return createNotFoundError(id);
}
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Todo>> deleteTodo(@PathVariable Long id) {
boolean isSuccess = todos.removeIf(todo -> todo.getId().equals(id));
if (isSuccess) {
return ResponseEntity.ok(new ApiResponse<>(true, null, null));
}
return createNotFoundError(id);
}
private ResponseEntity<ApiResponse<Todo>> createNotFoundError(Long id) {
ApiResponse.ErrorDetails error = new ApiResponse.ErrorDetails(
"https://example.com/errors/not-found",
"Todo not found",
HttpStatus.NOT_FOUND,
MessageFormat.format("Todo with id {0} does not exist", id),
MessageFormat.format("/api/todos/{0}", id)
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiResponse<>(false, null, error));
}
}
其實程式碼還有蠻多可以重構的,這邊只是示範概念,有興趣可以再自行研究
例如使用 Java 常用的
of
方法來建立 ApiResponse 物件
我們的設計與 RFC 7807 有以下相同點和差異點
設計一個統一的 API 回應格式可以顯著提高 API 的可用性和一致性
雖然它可能增加一些複雜性,但長期來看,這種方法通常會帶來更多好處,特別是在大型或長期維護的項目中
我的粉絲專頁
圖片來源:AI 產生